package com.woonoz.network;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.FunctionObject;
import org.mozilla.javascript.Scriptable;

public class PACInterpreter {

	private enum ProxyDeclarationType {PROXY, DIRECT}
	
	private final Function function;
	private final Scriptable scope;
	private final Context context;
	
	public PACInterpreter(final InputStream pacStream) throws InvalidPacException, PacInterpreterException {
		context = ContextFactory.getGlobal().enterContext();
        scope = context.initStandardObjects();
        
        declareFunction("myIpAddress", scope);
        declareFunction("dnsResolve", scope, String.class);
        
        try {
    		evaluateFileInContext(context, scope, this.getClass().getResourceAsStream("pacUtils.js"));        	
        } catch (IOException e) {
			throw new PacInterpreterException(e);
		}

        try {
    		evaluateFileInContext(context, scope, pacStream);        	
        } catch (IOException e) {
        	throw new InvalidPacException(e);
        }
        
    	final Object function = scope.get("FindProxyForURL", scope);
    	if (function instanceof Function) {
    		this.function = (Function) function;
    	} else {
    		throw new InvalidPacException("FindProxyForURL is not defined in given pac file");
    	}
	}
	
	public Collection<Proxy> getProxyForUrl(final String url, final String host) {
		final String proxyList = (String) function.call(context, scope, scope, new Object[] {url, host, scope});
		return parseProxyString(proxyList);
	}
	

	private Collection<Proxy> parseProxyString(final String proxyList) {
		final ProxyStringParser parser = new ProxyStringParser(proxyList);
		parser.parse();
		return parser.getProxies();
	}
	
	private static void evaluateFileInContext(final Context cx, final Scriptable scope, final InputStream input) throws IOException {
		InputStreamReader reader = new InputStreamReader(input);
    	cx.evaluateReader(scope, reader, "Proxy Auto Configuration File", 0, null);		
	}
	
	private void declareFunction(final String function, final Scriptable scope, Class<?>... classes) throws PacInterpreterException {
		FunctionObject f;
		try {
			f = new FunctionObject(function, this.getClass().getMethod(function, classes), scope);
		} catch (SecurityException e) {
			throw new PacInterpreterException(e);
		} catch (NoSuchMethodException e) {
			throw new PacInterpreterException(e);
		}
		scope.put(function, scope, f);
	}
	
	public static String dnsResolve(final String hostname) {
		try {
			final InetAddress address = Inet4Address.getByName(hostname);
			return address.getHostAddress();
		} catch (UnknownHostException e) {
			return null;
		}
	}
	
	public static String myIpAddress() {
		try {
			final InetAddress address = Inet4Address.getLocalHost();
			final String addr = address.getHostAddress();
			NetworkLogger.LOGGER.info(addr);
			return addr;
		} catch (UnknownHostException e) {
			return "127.0.0.1";
		}
	}
	
	private class ProxyStringParser {
		
		final String proxyString;
		final Set<Proxy> proxies;
		
		public ProxyStringParser(final String proxyString) {
			this.proxyString = proxyString;
			proxies = new HashSet<Proxy>();
		}
		
		public Collection<Proxy> getProxies() {
			return proxies;
		}
		
		public void parse() {
			final StringTokenizer semiColonTokenizer = new StringTokenizer(proxyString, ";");
			while (semiColonTokenizer.hasMoreTokens()) {
				final String proxyToParse = semiColonTokenizer.nextToken().trim();
				final StringTokenizer spaceTokenizer = new StringTokenizer(proxyToParse);
				
				if (! spaceTokenizer.hasMoreTokens()) {
					//ignore invalid proxy string
					continue;
				}
				
				final String proxyType = spaceTokenizer.nextToken();
				final ProxyDeclarationType type = ProxyDeclarationType.valueOf(proxyType);
				if (type == null) {
					//ignore invalid proxy string
					continue;
				}
				
				switch (type) {
				case DIRECT:
				{
					//Direct doesn't need more parameters
					proxies.add(Proxy.NO_PROXY);
					break;
				}
				case PROXY:
				{
					if (!spaceTokenizer.hasMoreTokens()) {
						//ignore invalid proxy string
						continue;
					}
					final Proxy proxy = handleNonNullProxy(spaceTokenizer.nextToken(), Proxy.Type.HTTP);
					if (proxy != null) {
						proxies.add(proxy);
					}
				}
				
				default:
					break;
				}
			}
		}
		
		private Proxy handleNonNullProxy(final String data, final Proxy.Type type) {
			
			final String[] parameters = data.split(":");
			
			if (parameters.length != 2) {
				return null;
			}
			
			try {
				final int port = Integer.parseInt(parameters[1]);
				final InetSocketAddress address = new InetSocketAddress(parameters[0], port);
				final Proxy proxy = new Proxy(type, address);
				return proxy;
			} catch (NumberFormatException e) {
				return null;
			}
		}
	}
}
